# Copyright 2010 Noam Yorav-Raphael
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

__all__ = ['BaseRecord', 'Record', 'FrozenRecord']

"""
A Record is an object whose value (for comparison and hashing) is made from
the fields it contains; Two different Record instances with equal field
values are considered equal. This is in contrast with Python's default classes,
where each instance is considered unique.

This allows for a meaningful repr(), comparison and hashing, which are given
automatically by the base class.
"""

from itertools import izip, chain
from inspect import getargspec

class RecordMetaclass(type):
    def __init__(cls, name, bases, dct):
        super(RecordMetaclass, cls).__init__(name, bases, dct)
        if hasattr(cls, '__abstract__'):
            del cls.__abstract__
            return
        if hasattr(cls, '__varargs__') or hasattr(cls, '__varkw__'):
            raise ValueError("__varargs__ and __varkw__ can't be set manually")
        if not hasattr(cls, '__fields__'):
            args, varargs, varkw, defaults = getargspec(dct['__init__'])
            if not args or args[0] != 'self':
                raise ValueError("First argument must be named 'self'")
            args = args[1:]
            if not all(isinstance(arg, str) for arg in args):
                raise ValueError("Nested args not allowed in Records")
            fields = list(args)
            if varargs:
                fields.append(varargs)
            cls.__varargs__ = bool(varargs)
            if varkw:
                fields.append(varkw)
            cls.__varkw__ = bool(varkw)
            cls.__fields__ = fields
        else:
            cls.__varargs__ = cls.__varkw__ = False
                
        if cls.__varargs__ or cls.__varkw__:
            if not getattr(cls, '__shortrepr__', True):
                raise ValueError("Can't have long repr if *args or **kwargs "
                                 "is used")
            cls.__shortrepr__ = True
        else:
            cls.__shortrepr__ = getattr(cls, '__shortrepr__', False)

class BaseRecord(object):
    """An abstract base class"""
    __metaclass__ = RecordMetaclass
    __abstract__ = True
    
    def __init__(self, lcls):
        if self.__class__ is BaseRecord:
            raise TypeError("BaseRecord is an abstract class")
        for field in self.__fields__:
            setattr(self, field, lcls[field])
    
    def _get_tuple(self):
        # Get a tuple with the record contents.
        return tuple(getattr(self, field) for field in self.__fields__)

    def __cmp__(self, other):
        return cmp(self._get_tuple(), other._get_tuple())

    def __repr__(self):
        shortrepr = self.__shortrepr__
        fields = self.__fields__
        varargs = self.__varargs__
        varkw = self.__varkw__
        if not shortrepr:
            return '%s(%s)' % (self.__class__.__name__,
                               ', '.join('%s=%r' % (field, getattr(self, field))
                                         for field in fields))
        else:
            args = [getattr(self, field) for field in fields]
            if varkw:
                kwargs = args.pop()
            else:
                kwargs = {}
            if varargs:
                args.extend(args.pop())
            sargs = chain((repr(x) for x in args),
                          ('%s=%r' % (k, v) for k, v in sorted(kwargs.iteritems())))
            return '%s(%s)' % (self.__class__.__name__, ', '.join(sargs))

class Record(BaseRecord):
    """A mutable, and therefore unhashable, record"""
    __abstract__ = True

    def __init__(self, lcls):
        if self.__class__ is Record:
            raise TypeError("Record is an abstract class")
        BaseRecord.__init__(self, lcls)

    def __hash__(self):
        raise TypeError("%r records are unhashable" % self.__class__.__name__)

class FrozenRecord(BaseRecord):
    """An immutable and hashable record"""
    __abstract__ = True

    def __init__(self, lcls):
        if self.__class__ is FrozenRecord:
            raise TypeError("FrozenRecord is an abstract class")
        BaseRecord.__init__(self, lcls)
        self._frozen = True

    def __setattr__(self, name, value):
        if getattr(self, '_frozen', False):
            raise TypeError("%r records are immutable" %
                            self.__class__.__name__)
        else:
            return BaseRecord.__setattr__(self, name, value)

    def __hash__(self):
        return hash(self._get_tuple())

